/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.dq.data.service.impl;


import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.Attribute;
import org.unidata.mdm.core.type.data.DataRecord;
import org.unidata.mdm.data.context.GetMultipleRequestContext;
import org.unidata.mdm.data.serialization.DataSerializer;
import org.unidata.mdm.data.service.DataRecordsService;
import org.unidata.mdm.data.type.data.EtalonRecord;
import org.unidata.mdm.data.type.data.EtalonRecordInfoSection;
import org.unidata.mdm.data.type.data.impl.EtalonRecordImpl;
import org.unidata.mdm.data.type.keys.RecordEtalonKey;
import org.unidata.mdm.dq.core.service.DataQualityService;
import org.unidata.mdm.dq.data.dao.SandboxRecordsDAO;
import org.unidata.mdm.dq.data.dao.po.SandboxRecordPO;
import org.unidata.mdm.dq.data.service.SandboxRecordsService;
import org.unidata.mdm.meta.util.ModelUtils;
import org.unidata.mdm.search.context.SearchRequestContext;
import org.unidata.mdm.search.dto.SearchResultDTO;
import org.unidata.mdm.search.dto.SearchResultHitDTO;
import org.unidata.mdm.search.dto.SearchResultHitFieldDTO;
import org.unidata.mdm.system.service.PlatformConfiguration;


@Service
public class SandboxRecordsServiceImpl implements SandboxRecordsService {

    private static final Logger logger = LoggerFactory.getLogger(SandboxRecordsServiceImpl.class);

    private final PlatformConfiguration platformConfiguration;

    private final SandboxRecordsDAO dqSandboxDao;

    private final DataQualityService dataQualityService;

    private final DataRecordsService dataRecordsService;

    private final MetaModelService metaModelService;

    @Autowired
    public SandboxRecordsServiceImpl(
            final PlatformConfiguration platformConfiguration,
            final SandboxRecordsDAO dqSandboxDao,
            final DataQualityService dataQualityExecutionService,
            final DataRecordsService dataRecordsService,
            final MetaModelService metaModelService
    ) {
        this.platformConfiguration = platformConfiguration;
        this.dqSandboxDao = dqSandboxDao;
        this.dataQualityService = dataQualityExecutionService;
        this.dataRecordsService = dataRecordsService;
        this.metaModelService = metaModelService;
    }

    @Transactional
    @Override
    public DataRecord upsert(final DataRecord record) {

//        final byte[] data = DataSerializer.toProtostuff(record);
//        if (data == null) {
//            logger.error("Data is null for {}", record.getInfoSection().getEtalonKey().getId());
//        }

//        final EtalonRecordInfoSection infoSection = record.getInfoSection();
//        final Long recordId =
//                infoSection.getEtalonKey().getId() != null ? Long.valueOf(infoSection.getEtalonKey().getId()) : null;
//        final long id = dqSandboxDao.save(new SandboxRecordPO(recordId, infoSection.getEntityName(), data));
        return null; // updateEtalonKey(id, record);
    }

    @Transactional
    @Override
    public EtalonRecord findRecordById(long recordId) {
        final SandboxRecordPO sandboxRecord = dqSandboxDao.findRecordById(recordId);
        return toEtalonRecord(sandboxRecord);
    }

    @Transactional
    @Override
    public void deleteRecords(final List<Long> recordsIds, final String entityName) {
        if (CollectionUtils.isNotEmpty(recordsIds)) {
            dqSandboxDao.deleteByIds(recordsIds);
        } else if (StringUtils.isNoneBlank(entityName)) {
            dqSandboxDao.deleteByEntityName(entityName);
        }
    }

    @Transactional
    @Override
    public SearchResultDTO searchRecords(final SearchRequestContext searchRequestContext) {
        final String entityName = searchRequestContext.getEntity();
        final List<SandboxRecordPO> sandboxRecordPOS = dqSandboxDao.find(
                entityName,
                searchRequestContext.getPage(),
                searchRequestContext.getCount()
        );
        final SearchResultDTO searchResultDTO = new SearchResultDTO();
        searchResultDTO.setTotalCount(dqSandboxDao.count(entityName));
        searchResultDTO.setHits(
                sandboxRecordPOS.stream()
                        .map(sr -> extractSearchHit(sr, searchRequestContext.getReturnFields()))
                        .collect(Collectors.toList())
        );
        return searchResultDTO;
    }

    private SearchResultHitDTO extractSearchHit(final SandboxRecordPO sandboxRecordPO, final List<String> returnFields) {
        final EtalonRecord etalonRecord = toEtalonRecord(sandboxRecordPO);
        final Map<String, SearchResultHitFieldDTO> hits = CollectionUtils.isNotEmpty(returnFields) ?
                returnFields.stream()
                        .map(field -> Pair.of(field, toHitField(etalonRecord, field)))
                        .collect(Collectors.toMap(Pair::getKey, Pair::getValue)) :
                Collections.emptyMap();
        return new SearchResultHitDTO(
                String.valueOf(sandboxRecordPO.getId()),
                String.valueOf(sandboxRecordPO.getId()),
                .0f,
                hits,
                null
        );
    }

    private SearchResultHitFieldDTO toHitField(final EtalonRecord etalonRecord, final String field) {
        final boolean compoundPath = ModelUtils.isCompoundPath(field);
        return new SearchResultHitFieldDTO(
                field,
                compoundPath ?
                        extractCompoundValues(etalonRecord.getAttributeRecursive(field)) :
                        extractSigleValue(etalonRecord.getAttribute(field))
        );
    }

    private List<Object> extractCompoundValues(final Collection<Attribute> attributes) {
        return attributes.stream()
                .flatMap(attribute -> extractSigleValue(attribute).stream())
                .collect(Collectors.toList());
    }

    private List<Object> extractSigleValue(Attribute attribute) {
        if (attribute instanceof org.unidata.mdm.core.type.data.SimpleAttribute) {
            return Collections.singletonList(((org.unidata.mdm.core.type.data.SimpleAttribute) attribute).getValue());
        } else if (attribute instanceof org.unidata.mdm.core.type.data.CodeAttribute) {
            return Collections.singletonList(((org.unidata.mdm.core.type.data.CodeAttribute) attribute).getValue());
        } else if (attribute instanceof org.unidata.mdm.core.type.data.ArrayAttribute) {
            return ((org.unidata.mdm.core.type.data.ArrayAttribute) attribute).getValues();
        }
        return Collections.emptyList();
    }

    private EtalonRecord toEtalonRecord(SandboxRecordPO sandboxRecord) {
        final byte[] data = sandboxRecord.getData();
        final DataRecord dataRecord = DataSerializer.fromProtostuff(data);
        final EtalonRecordImpl etalonRecord = new EtalonRecordImpl(dataRecord);
        final EtalonRecordInfoSection etalonRecordInfoSection = new EtalonRecordInfoSection();
        etalonRecordInfoSection.setEtalonKey(RecordEtalonKey.builder().id(String.valueOf(sandboxRecord.getId())).build());
        etalonRecordInfoSection.setEntityName(sandboxRecord.getEntityName());
        etalonRecord.setInfoSection(etalonRecordInfoSection);
        return etalonRecord;
    }

    private EtalonRecord updateEtalonKey(final long id, final EtalonRecord etalonRecord) {
        etalonRecord.getInfoSection().setEtalonKey(RecordEtalonKey.builder().id(String.valueOf(id)).build());
        return etalonRecord;
    }

//    @Override
//    public Map<EtalonRecord, List<DQDataRecordError>> runDataQualityRules(final RunDQRulesContext runDQRulesContext) {
//
//        final List<String> etalonRecordsIds = runDQRulesContext.getEtalonRecordsIds();
//        if (CollectionUtils.isEmpty(etalonRecordsIds) || CollectionUtils.isEmpty(runDQRulesContext.getRules())) {
//            return Collections.emptyMap();
//        }
//
//        final String entityName = runDQRulesContext.getEntityName();
//        List<EtalonRecord> etalonRecords = runDQRulesContext.isSandbox() ?
//                findTestEtalonRecords(etalonRecordsIds) :
//                findRealEtalonRecords(etalonRecordsIds, entityName);
//
//        final Set<String> rulesNames = new HashSet<>(runDQRulesContext.getRules());
//
//        Map<String, AttributeElement> attributes = null;
//// FIXME Uncomment and repair
////        AbstractEntity entity = metaDraftService.getLookupEntityById(entityName);
////        if (Objects.isNull(entity)) {
////            GetEntityDTO info = metaDraftService.getEntityById(entityName);
////            if (Objects.nonNull(info)) {
////                entity = info.getEntity();
////                attributes = ModelUtils.createAttributesMap(entity, info.getNesteds());
////            }
////        } else {
////            attributes = ModelUtils.createAttributesMap(entity, Collections.emptyList());
////        }
//
////        dqService.getRules()
////        List<DQRuleDef> dqRuleDefs = Optional.ofNullable(entity).map(dqService.getRules(entity.getName(),entity.getName(), entity.))
//
//
//        List<QualityRuleSource> dqRules = Collections.emptyList();
//// FIXME Uncomment and repair
////                Optional.ofNullable(entity)
////                //.map(AbstractEntityDef::getDataQualities)
////                .map(e -> dqService.getRules(e).all())
////                .orElseGet(Collections::emptyList)
////                .stream()
////                .filter(r -> rulesNames.contains(r.getName()))
////                .collect(Collectors.toList());
//
//        // TODO: 30.10.2019 FOR DQ TESTING ONLY
//        //==============================================
////        if (Objects.isNull(entity) && CollectionUtils.isEmpty(dqRules)) {
////            dqRules = new ArrayList<>(dqService.getRules(entityName).all());
////            Collection<Attribute> recordAttributes = etalonRecords.get(0).getAllAttributes();
////            Map<String, AttributeModelElement> modelAttributes = null;
////            attributes = new HashMap<>();
////            AbstractEntity entityDef = new AbstractEntity();
////            entityDef.setName(entityName);
////            entityDef.setDisplayName(entityName);
////            for(Attribute a : recordAttributes){
////                attributes.put(a.getName(), new AttributeInfoHolder(new SimpleMetaModelAttribute()
////                        .withSimpleDataType(SimpleDataType.STRING)
////                        .withName(a.getName())
////                        , entityDef, null, a.toLocalPath(), 0));
////            }
//
//
////            AttributeModelElement a = new AttributeModelElement();
//            //attributes = etalonRecords.get(0).getAllAttributes()
////        }
//        //==============================================
//
//
//        if (CollectionUtils.isEmpty(dqRules)) {
//            return Collections.emptyMap();
//        }
//        final List<QualityRuleSource> dqRuleDefs = new ArrayList<>(dqRules);
//        // Init UPath elements
//        // TODO: 31.10.2019 prepareMappings to cach service
//
//        Date dt = new Date();
//        SourceSystemElement sse = metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement();
//        return etalonRecords.stream()
//                .map(etalonRecord -> {
//
//                    OriginRecord origin = new OriginRecordImpl()
//                            .withDataRecord(etalonRecord)
//                            .withInfoSection(new OriginRecordInfoSection()
//                                    .withCreateDate(dt)
//                                    .withCreatedBy(SecurityUtils.getCurrentUserName())
//                                    .withMajor(platformConfiguration.getPlatformMajor())
//                                    .withMinor(platformConfiguration.getPlatformMinor())
//                                    .withOriginKey(RecordOriginKey.builder()
//                                            .entityName(entityName)
//                                            .externalId(IdUtils.v1String())
//                                            .sourceSystem(sse.getName())
//                                            .status(RecordStatus.ACTIVE)
//                                            .build())
//                                    .withShift(DataShift.PRISTINE)
//                                    .withStatus(RecordStatus.ACTIVE)
//                                    .withUpdateDate(dt)
//                                    .withUpdatedBy(SecurityUtils.getCurrentUserName())
//                                    .withValidFrom(etalonRecord.getInfoSection().getValidFrom())
//                                    .withValidTo(etalonRecord.getInfoSection().getValidTo()));
//                    // CalculableHolder.of(origin);
//
//                    DataQualityContext dCtx = DataQualityContext.builder()
//                            .sourceSystem(sse.getName())
//                            .input(StringUtils.EMPTY, origin)
//                            .build();
//
//                    dataQualityService.apply(dCtx);
//
//                    //ModificationBox<OriginRecord> box = dCtx.getModificationBox();
//                    EtalonRecord result = new EtalonRecordImpl()
//                            //.withDataRecord(box.getCalculationState())
//                            .withInfoSection(etalonRecord.getInfoSection());
//
//                    return Pair.of(
//                            result,
//                            CollectionUtils.isNotEmpty(dCtx.getErrors()) ?
//                                    dCtx.getErrors().stream().map(DQDataRecordError::new).collect(Collectors.toList()) : Collections.<DQDataRecordError>emptyList()
//                    );
//                })
//                .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
//    }


    private List<EtalonRecord> findRealEtalonRecords(final List<String> etalonRecordsIds, final String entityName) {
        final GetMultipleRequestContext multipleRequestContext = GetMultipleRequestContext.builder()
                .entityName(entityName)
                .etalonKeys(etalonRecordsIds)
                .build();
        return dataRecordsService.getRecords(multipleRequestContext).getEtalons();
    }

    private List<EtalonRecord> findTestEtalonRecords(final List<String> etalonRecordsIds) {
        return dqSandboxDao.findByRecordsIds(etalonRecordsIds.stream().map(Long::valueOf).collect(Collectors.toList()))
                .stream()
                .map(sr -> updateEtalonKey(sr.getId(), toEtalonRecord(sr)))
                .collect(Collectors.toList());
    }
}
